/*
 * Tranquil Java Integrated Development Environment
 *
 * The GNU General Public License Version 3
 *
 * Copyright (C) 2021 Autumn Lamonte
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Autumn Lamonte [AutumnWalksTheLake@gmail.com] ⚧ Trans Liberation Now
 * @version 1
 */
package tjide.project;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.HashSet;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipException;
import java.util.regex.PatternSyntaxException;

import tjide.build.CompileListener;
import tjide.build.CompileTask;

/**
 * JarTarget combines several JavaTargets into one Java archive file (JAR).
 */
public class JarTarget extends ArchiveTarget implements CompileTask {

    // ------------------------------------------------------------------------
    // Variables --------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * The list of file match strings (regular expressions) to include in
     * this JAR.
     */
    private List<String> filters = new ArrayList<String>();

    /**
     * The value to use for Implementation-Version.
     */
    private String implementationVersion = "";

    /**
     * The value to use for Main-Class.
     */
    private String mainClass = "";

    /**
     * Compile start time.
     */
    private long compileStartTime;

    /**
     * Compile running flag.
     */
    private boolean compileRunning = false;

    // ------------------------------------------------------------------------
    // Constructors -----------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Public constructor.
     *
     * @param jarFilename the name of the generated file of the target,
     * relative to the project's build output directory
     */
    public JarTarget(final String jarFilename) {
        super(jarFilename, jarFilename);
    }

    // ------------------------------------------------------------------------
    // ArchiveTarget ----------------------------------------------------------
    // ------------------------------------------------------------------------

    // ------------------------------------------------------------------------
    // JarTarget --------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Make human-readable description of this target.
     *
     * @return displayable String
     */
    @Override
    public String toString() {
        return "JAR: " + name;
    }

    /**
     * Add a filter string identifying files or classes to include in this
     * JAR file.
     *
     * @param filter a regular expression to match
     */
    public void addFilter(final String filter) {
        filters.add(filter);
    }

    /**
     * Get the list of filters defined for this JAR.
     *
     * @return the list of filters
     */
    public List<String> getFilters() {
        return new ArrayList<String>(filters);
    }

    /**
     * Set the list of filters defined for this JAR.
     *
     * @param filters the new filters
     */
    public void setFilters(final List<String> filters) {
        this.filters = new ArrayList<String>(filters);
    }

    /**
     * Get the Implementation-Version string.
     *
     * @return the Implementation-Version
     */
    public String getImplementationVersion() {
        return implementationVersion;
    }

    /**
     * Set the Implementation-Version string.
     *
     * @param implementationVersion the Implementation-Version
     */
    public void setImplementationVersion(final String implementationVersion) {
        this.implementationVersion = implementationVersion;
    }

    /**
     * Get the Main-Class string.
     *
     * @return the Main-Class
     */
    public String getMainClass() {
        return mainClass;
    }

    /**
     * Set the Main-Class string.
     *
     * @param mainClass the Main-Class
     */
    public void setMainClass(final String mainClass) {
        this.mainClass = mainClass;
    }

    /**
     * Combine the targets from project that match filters into a JAR file.
     *
     * @param project the project metadata
     */
    public void compile(final Project project) {
        compileStartTime = System.currentTimeMillis();
        compileRunning = true;

        for (CompileListener listener: compileListeners) {
            listener.setCompileBegin();
            listener.setCompileTask(this);
            listener.setCompileFile(name, buildFilename);
            listener.setCompileLineCount(0);
            listener.setCompileAvailableMemory(Runtime.getRuntime().
                freeMemory() / 1024);
        }

        File jarFile = new File(project.getRootDir(), buildFilename);

        // System.err.println("create jar: " + jarFile);

        List<Target> targets = project.getTargets();
        HashSet<String> filenames = new HashSet<String>();

        String resourcesDirStr = project.getFullResourcesDir();
        File resourcesDir = new File(resourcesDirStr);
        String buildDirStr = project.getFullBuildDir();
        File buildDir = new File(buildDirStr);

        for (String filter: filters) {
            if (filter.equals("*")) {
                // Special case: everyone uses "*", so just support it.
                filter = ".*";
            }
            // Add the targets that match the pattern.
            for (Target target: targets) {
                if (target instanceof FileTarget) {
                    String filename = target.getBuildFilename();
                    try {
                        if (filename.matches(filter)) {
                            // System.err.println("Target add: " + filename);
                            filenames.add(new File(project.getFullBuildDir(),
                                    filename).toString());
                        }
                    } catch (PatternSyntaxException e) {
                        // SQUASH
                    }
                }
            }
            // Now search the build directory for all files matching the
            // pattern.
            List<String> allFiles = getFiles(buildDir);
            for (String filename: allFiles) {
                try {
                    if (filename.matches(filter)) {
                        // System.err.println("Scan add: " + filename);
                        filenames.add(filename);
                    }
                } catch (PatternSyntaxException e) {
                    // SQUASH
                }
            }

            // Add everything in the resources dir.
            allFiles = getFiles(resourcesDir);
            for (String filename: allFiles) {
                try {
                    if (filename.matches(filter)) {
                        // System.err.println("Scan add: " + filename);
                        filenames.add(filename);
                    }
                } catch (PatternSyntaxException e) {
                    // SQUASH
                }
            }
        }

        try {
            Manifest manifest = new Manifest();
            Attributes attributes = manifest.getMainAttributes();
            attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0.0");
            if (mainClass.trim().length() > 0) {
                attributes.put(Attributes.Name.MAIN_CLASS,
                    mainClass.trim());
            }
            if (implementationVersion.trim().length() > 0) {
                attributes.put(Attributes.Name.IMPLEMENTATION_VERSION,
                    implementationVersion.trim());
            }

            JarOutputStream output = new JarOutputStream(new
                FileOutputStream(jarFile), manifest);

            for (String filename: filenames) {
                String jarFilename = filename;
                if (jarFilename.startsWith(buildDirStr + File.separator)) {
                    jarFilename = jarFilename.substring(buildDirStr.length() + 1);
                } else if (jarFilename.startsWith(resourcesDirStr +
                        File.separator)
                ) {
                    jarFilename = jarFilename.substring(resourcesDirStr.length() + 1);
                }
                // Zip entries must use POSIX directory slashes.
                jarFilename = jarFilename.replace(File.separator, "/");

                // System.err.println("  ++ " + jarFilename);
                addToJar(jarFilename, new File(filename), output);
            }

            // If FatJAR is enabled, copy every entry of every jar from the
            // project into this jar.
            if (project.getBuildFatJar()) {
                makeFatJar(project, output);
            }

            // All done.
            output.close();

            for (CompileListener listener: compileListeners) {
                listener.setCompileSucceeded(true);
            }

        } catch (IOException e) {
            project.displayException(e);

            for (CompileListener listener: compileListeners) {
                listener.setCompileFailed(true);
            }
        } finally {
            compileRunning = false;
        }

    }

    /**
     * Add a file to a jar.
     *
     * @param project the project metadata
     * @param output the jar to add filename to
     * @throws IOException if an I/O error occurs
     */
    private void makeFatJar(final Project project,
        final JarOutputStream output) throws IOException {

        for (String jar: project.getJars()) {
            JarFile inFile = new JarFile(new File(new File(project.getRootDir(),
                        project.getResourcesDir()), jar), false,
                JarFile.OPEN_READ);

            Enumeration<JarEntry> entries = inFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                try {
                    output.putNextEntry(entry);
                } catch (ZipException e) {
                    // Skip over duplicate or erroneous files.  First one in
                    // won.
                    continue;
                }

                InputStream input = inFile.getInputStream(entry);
                byte [] buffer = new byte[1024];
                while (true) {
                    int n = input.read(buffer, 0, buffer.length);
                    if (n <= 0) {
                        break;
                    }
                    output.write(buffer, 0, n);
                }
                input.close();
            }
        }
    }

    /**
     * Add a file to a jar.
     *
     * @param filename the name of the file to add
     * @param inputFile the file containing the data
     * @param output the jar to add filename to
     * @throws IOException if an I/O error occurs
     */
    private void addToJar(final String filename, final File inputFile,
        final JarOutputStream output) throws IOException {

        JarEntry entry = new JarEntry(filename);
        entry.setTime(inputFile.lastModified());
        output.putNextEntry(entry);

        FileInputStream input = new FileInputStream(inputFile);
        byte [] buffer = new byte[1024];
        while (true) {
          int n = input.read(buffer, 0, buffer.length);
          if (n <= 0) {
              break;
          }
          output.write(buffer, 0, n);
        }
        input.close();
    }

    /**
     * Get the files in a directory, recursively.
     *
     * @param path the directory to search
     * @return the files in the directory
     */
    private List<String> getFiles(final File path) {
        List<String> result = new ArrayList<String>();
        if (!path.isDirectory()) {
            return result;
        }
        File [] files = path.listFiles();
        for (File file: files) {
            if (file.isFile()) {
                result.add(file.toString());
            }
            if (file.isDirectory()) {
                result.addAll(getFiles(file));
            }
        }
        return result;
    }

    // ------------------------------------------------------------------------
    // CompileTask ------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Cancel an in-progress compile.  Does nothing if the compile is not
     * actually running.  Note that the internal compiler cannot be canceled.
     */
    public void cancelCompile() {
        // NOP
    }

    /**
     * Determine if compile is in progress.
     *
     * @return true if the compile is in progress
     */
    public boolean isCompileRunning() {
        return compileRunning;
    }

    /**
     * Get the start time of the compile task.
     *
     * @return the system time (millis) the compile task started
     */
    public long getCompileStartTime() {
        return compileStartTime;
    }

    /**
     * Get the target of the compile task.
     *
     * @return the target of the compile task
     */
    public Target getTarget() {
        return this;
    }

}
